﻿using System;
using System.Collections.Generic;
using System.Globalization;
using System.IO;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using HtmlAgilityPack;
using LcboServices.BusinessEntities;
using Locator.LcboServices.PageLoaders.PageRetrievers;
using Locator.LcboServices.PageLoaders.Properties;
using Locator.LcboServices.PageLoaders.Services;
using Locator.LcboServices.PageLoaders.Interface;
using Microsoft.Practices.CompositeWeb;
using Microsoft.Practices.EnterpriseLibrary.Logging;
using Microsoft.Practices.EnterpriseLibrary.Common.Configuration;

namespace Locator.LcboService.Services
{
    internal class InventoryRetriever : IInventoryRetriever
    {
        private readonly LogWriter logWriter;

        private readonly IPageRetriever pageRetriever;

        private readonly TraceManager traceManager;

        private List<LcboInventoryData2>[] results;

        private Dictionary<int, List<LcboInventoryData2>> resultsDict;

        private List<HtmlDocument> theDocs;

        private List<string> theStrings;

        public InventoryRetriever
            (
            [ServiceDependency]IPageRetrieverFactory pageRetrieverFactory
            )
        {
            this.pageRetriever = pageRetrieverFactory.InventoryPageRetriever;
            this.logWriter = EnterpriseLibraryContainer.Current.GetInstance<LogWriter>();
            this.traceManager = new TraceManager(this.logWriter);
        }

        public static int ExtractInventoryLevel(string line)
        {
            int result = 0;
            int startOfFontTag = line.IndexOf("<font ", StringComparison.OrdinalIgnoreCase);
            int endOfStartFontTag = line.IndexOf(">", startOfFontTag, StringComparison.OrdinalIgnoreCase);
            int endOfFontTag = line.IndexOf("</font>", StringComparison.OrdinalIgnoreCase);
            string value = line.Substring(endOfStartFontTag + 1, endOfFontTag - endOfStartFontTag - 1);
            result = Convert.ToInt32(value);
            return result;
        }

        public List<LcboInventoryData2> RetrieveMatches2(string productNumber)
        {
            List<LcboInventoryData2> result = new List<LcboInventoryData2>();
            //Dictionary<string, LcboInventoryData2> inventoryDictionary = new Dictionary<string, LcboInventoryData2>();
            //Dictionary<string, HtmlNode> colEntries = new Dictionary<string, HtmlNode>();
            HtmlDocument myPageSource;
            try
            {
                myPageSource = this.pageRetriever.RetrievePageHtml(productNumber);
            }
            catch (Exception ex)
            {
                //Trace.TraceError(ex.ToString());
                myPageSource = this.pageRetriever.RetrievePageHtml(String.Empty);
            }

            //string itemAnchorIdPrefix = "col";
            HtmlNodeCollection colsixes = myPageSource.DocumentNode.SelectNodes("//a[@id[starts-with(.,'col')]]");
            foreach (HtmlNode link in colsixes)
            {
                result.Add(ExtractLcboInventoryData(productNumber, link));
            }

            return result;
        }

        public List<LcboInventoryData2> RetrieveMatches3(IEnumerable<int> productIds)
        {
            Task<HtmlDocument>[] theTasks = null;
            this.theDocs = new List<HtmlDocument>();
            ManualResetEvent[] syncEvent = new ManualResetEvent[] { new ManualResetEvent(false) };
            //Trace.TraceInformation("About to call RetrievePageHtmlsAsync");
            //pageRetrivalStopWatch = new Stopwatch();
            //pageRetrivalStopWatch.Start();
            this.pageRetriever.RetrievePageHtmlsAsync(productIds.ToArray(), (docsTasks) =>
            {
                //Trace.TraceInformation("ContinueWithAll callback started");
                theTasks = docsTasks;
                //Task.WaitAll(docsTasks);
                Array.ForEach(docsTasks, task =>
                {
                    if (task.Exception == null)
                        this.theDocs.Add(task.Result);
                });

                syncEvent[0].Set();
            });

            //Trace.TraceInformation("Finished call to RetrievePageHtmlsAsync, starting to wait");
            WaitHandle.WaitAll(syncEvent);
            //Trace.TraceInformation("Finished waiting after RetrievePageHtmlsAsync after {0} milliseconds", pageRetrivalStopWatch.ElapsedMilliseconds);
            //if (theTasks != null)
            //{
            //    //Task.WaitAll(theTasks);
            //    foreach (Task<HtmlDocument> task in theTasks)
            //    {
            //        if (task.Exception == null)
            //        {
            //            theDocs.Add(task.Result);
            //        }
            //    }
            //}


            resultsDict = new Dictionary<int, List<LcboInventoryData2>>();
            List<LcboInventoryData2> result = new List<LcboInventoryData2>();
            int count = 0;
            foreach (int productId in productIds)
            {
                count++;
                resultsDict.Add(productId, null);
            }

            results = new List<LcboInventoryData2>[count];
            Task[] tasks = new Task[count];
            count = 0;
            foreach (int productId in productIds)
            {
                object productIdObject = productId;
                object index = count;
                Task t = Task.Factory.StartNew(() => this.RetMatch9(productIdObject, index));
                tasks[count] = t;
                count++;
            }

            Task.WaitAll(tasks);

            foreach (List<LcboInventoryData2> res in resultsDict.Values)
            {
                result.AddRange(res);
            }

            //this.pageRetrivalStopWatch.Stop();
            //Trace.TraceInformation("Finished RetrieveMatches3 after {0} milliseconds", this.pageRetrivalStopWatch.ElapsedMilliseconds);
            return result;
        }

        public List<LcboInventoryData2> RetrieveMatches3WithStrings(IEnumerable<int> productIds)
        {
            Task<string>[] theTasks = null;
            this.theStrings = new List<string>();
            ManualResetEvent[] syncEvent = new ManualResetEvent[] { new ManualResetEvent(false) };
            Action<Task<string>[]> processStrings = (stringsTasks) =>
            {
                //Trace.WriteLine(String.Format(CultureInfo.CurrentCulture, "{0} ContinueWithAll callback started", DateTime.Now.ToString("MM/dd/yyyy hh:mm:ss.fff tt", CultureInfo.CurrentCulture)));
                theTasks = stringsTasks;
                //Task.WaitAll(docsTasks);
                Array.ForEach(stringsTasks, task =>
                {
                    if (task.Exception == null)
                        this.theStrings.Add(task.Result);
                });

                syncEvent[0].Set();
            };

            //Trace.WriteLine(String.Format(CultureInfo.CurrentCulture, "{0} About to call RetrievePageHtmlsAsync", DateTime.Now.ToString("MM/dd/yyyy hh:mm:ss.fff tt", CultureInfo.CurrentCulture)));
            this.pageRetriever.RetrievePageStringsAsync(productIds.ToArray(), processStrings);
            //Trace.WriteLine(String.Format(CultureInfo.CurrentCulture, "{0} Finished call to RetrievePageHtmlsAsync, starting to wait", DateTime.Now.ToString("MM/dd/yyyy hh:mm:ss.fff tt", CultureInfo.CurrentCulture)));
            WaitHandle.WaitAll(syncEvent);
            //Trace.WriteLine(String.Format(CultureInfo.CurrentCulture, "{0} Finished waiting", DateTime.Now.ToString("MM/dd/yyyy hh:mm:ss.fff tt", CultureInfo.CurrentCulture)));
            //if (theTasks != null)
            //{
            //    //Task.WaitAll(theTasks);
            //    foreach (Task<HtmlDocument> task in theTasks)
            //    {
            //        if (task.Exception == null)
            //        {
            //            theDocs.Add(task.Result);
            //        }
            //    }
            //}


            resultsDict = new Dictionary<int, List<LcboInventoryData2>>();
            List<LcboInventoryData2> result = new List<LcboInventoryData2>();
            int count = 0;
            foreach (int productId in productIds)
            {
                count++;
                resultsDict.Add(productId, null);
            }

            results = new List<LcboInventoryData2>[count];
            Task[] tasks = new Task[count];
            count = 0;
            foreach (int productId in productIds)
            {
                object productIdObject = productId;
                object index = count;
                Task t = Task.Factory.StartNew(() => this.RetMatch10(productIdObject, index));
                tasks[count] = t;
                count++;
            }

            Task.WaitAll(tasks);
            //TODO: Do this async

            foreach (List<LcboInventoryData2> res in resultsDict.Values)
            {
                result.AddRange(res);
            }

            return result;
        }

        private static string ExtractCity(HtmlNode link)
        {
            string result = link.FirstChild.InnerText;
            if (result.EndsWith("-CENTRAL", StringComparison.CurrentCultureIgnoreCase) || result.EndsWith("-DOWNTOWN", StringComparison.CurrentCultureIgnoreCase))//("TORONTO", StringComparison.CurrentCultureIgnoreCase))
            {
                result = result.Substring(0, result.IndexOf("-"));
            }
            else
            {
                result = result.Substring(result.IndexOf("-") + 1);
            }

            return result;
        }

        private static int ExtractInventoryLevel(HtmlNode link)
        {
            int level = 0;
            Int32.TryParse(link.FirstChild.InnerText, out level);
            return level;
        }

        private static LcboInventoryData2 ExtractLcboInventoryData(string productNumber, HtmlNode link)
        {
            HtmlAttribute linkId = link.Attributes["id"];
            LcboInventoryData2 currentInventoryData = new LcboInventoryData2();
            if (linkId.Value.EndsWith("6"))
            {
                currentInventoryData.ProductId = Convert.ToInt32(productNumber);
                currentInventoryData.NumberInStock = InventoryRetriever.ExtractInventoryLevel(link);
                string store = "?STORE=";
                string amp = "&amp;";
                int storeTextPosition = link.Attributes["href"].Value.IndexOf(store);
                int amppos = link.Attributes["href"].Value.IndexOf(amp);
                string storenumber = link.Attributes["href"].Value.Substring(storeTextPosition + store.Length, amppos - storeTextPosition - store.Length);
                currentInventoryData.StoreNumber = Convert.ToInt32(storenumber);
            }

            return currentInventoryData;
        }

        private static string ExtractStreetAddress(HtmlNode link)
        {
            string address = link.InnerText;
            //string exceptionalAddress;
            //if (InventoryRetriever.exceptionalLocations.TryGetValue(address, out exceptionalAddress))
            //{
            //    address = exceptionalAddress;
            //}

            return address;
        }

        private List<LcboInventoryData2> ParseDocument(HtmlDocument doc, int productNumber)
        {
            //Stopwatch parseDocumentStopWatch = new Stopwatch();
            //parseDocumentStopWatch.Start();

            Dictionary<string, LcboInventoryData2> resultDictionary = new Dictionary<string, LcboInventoryData2>();

            HtmlNodeCollection docDocumentNodeSelectNodes = doc.DocumentNode.SelectNodes(String.Format("//a[@id[starts-with(.,'{0}')]]", Resources.ItemAnchorIdPrefix));
            if (docDocumentNodeSelectNodes != null)
                foreach (HtmlNode link in docDocumentNodeSelectNodes)
                {
                    List<LcboInventoryData2> result = new List<LcboInventoryData2>();
                    HtmlAttribute linkId = link.Attributes["id"];
                    string idName = linkId.Value;
                    string idNumber = idName.Substring(Resources.ItemAnchorIdPrefix.Length, idName.Length - Resources.ItemAnchorIdPrefix.Length - 1);
                    LcboInventoryData2 currentInventoryData;
                    if (!resultDictionary.ContainsKey(idNumber))
                    {
                        currentInventoryData = new LcboInventoryData2();
                        resultDictionary.Add(idNumber, currentInventoryData);
                    }
                    else
                    {
                        currentInventoryData = resultDictionary[idNumber];
                    }

                    string store = "?STORE=";
                    string amp = "&amp;";
                    int storeTextPosition = link.Attributes[Resources.Href].Value.IndexOf(store);
                    int amppos = link.Attributes[Resources.Href].Value.IndexOf(amp);
                    string storenumber = link.Attributes[Resources.Href].Value.Substring(storeTextPosition + store.Length, amppos - storeTextPosition - store.Length);
                    currentInventoryData.StoreNumber = Convert.ToInt32(storenumber);
                    currentInventoryData.ProductId = productNumber;
                    switch (idName[idName.Length - 1])
                    {
                        case '1':
                            //City
                            currentInventoryData.City = InventoryRetriever.ExtractCity(link);
                            break;
                        case '2':
                            //Street address
                            currentInventoryData.StreetAddress = InventoryRetriever.ExtractStreetAddress(link);
                            break;
                        case '6':
                            //Inventory level
                            currentInventoryData.NumberInStock = InventoryRetriever.ExtractInventoryLevel(link);
                            break;
                        default:
                            break;
                    }
                }

            //Trace.TraceInformation("Finished processing collection for product ID {1} after {0} milliseconds", parseDocumentStopWatch.ElapsedMilliseconds, productNumber);
            //parseDocumentStopWatch.Stop();
            return new List<LcboInventoryData2>(resultDictionary.Values);
        }

        private List<LcboInventoryData2> ParseString(string html, int productId)
        {
            List<LcboInventoryData2> result = new List<LcboInventoryData2>();
            string target = "<a class=\"item-details-col6\"";
            using (StringReader reader = new StringReader(html))
            {
                string line;
                while ((line = reader.ReadLine()) != null)
                {
                    if (line.Contains(target))
                    {
                        LcboInventoryData2 currentInventoryData = new LcboInventoryData2();
                        currentInventoryData.ProductId = productId;
                        currentInventoryData.NumberInStock = InventoryRetriever.ExtractInventoryLevel(line);
                        string store = "?STORE=";
                        string amp = "&amp;";
                        int storeTextPosition = line.IndexOf(store);
                        int amppos = line.IndexOf(amp);
                        string storenumber = line.Substring(storeTextPosition + store.Length, amppos - storeTextPosition - store.Length);
                        currentInventoryData.StoreNumber = Convert.ToInt32(storenumber);
                        result.Add(currentInventoryData);
                    }
                }
            }

            return result;
        }

        private void RetMatch(object state, int index)
        {
            int productId = (int)state;
            this.resultsDict[productId] = this.RetrieveMatches2(productId.ToString(CultureInfo.InvariantCulture));
        }

        private void RetMatch10(object state, object index)
        {
            int productId = (int)state;
            int indexAsInt = (int)index;
            this.resultsDict[productId] = this.ParseString(this.theStrings[indexAsInt], productId);
        }

        private void RetMatch9(object state, object index)
        {
            int productId = (int)state;
            int indexAsInt = (int)index;
            this.resultsDict[productId] = this.ParseDocument(this.theDocs[indexAsInt], productId);
            //Trace.TraceInformation("Finished parsing document for product ID {0} after {1} milliseconds", productId, this.pageRetrivalStopWatch.ElapsedMilliseconds);
        }
    }
}
